# -*- coding:utf-8 -*-
import numpy as np
import time


class Ising:
    """
    A class to implement Ising models.

    ...

    Attributes
    ----------
    J : numpy.ndarray
        spin interactions matrix (must be semi-definite positive)
    h : numpy.ndarray
        magnectic field effect vector
    dimension : int
        number of spins
    ground_state : numpy.ndarray
        vector of spins orientation to minimize the energy
    optimization_logs : dict
        data about the optimization of the model
    """

    def __init__(self, J: np.ndarray, h: np.ndarray, assert_parameters: bool = True) -> None:

        """
        Constructs all the necessary attributes for the Ising object.

        Parameters
        ----------
            J : numpy.ndarray
                spin interactions matrix (must be semi-definite positive)
            h : numpy.ndarray
                magnectic field effect vector
            assert_parameters : bool, optional
                check the format of the inputs (default is True)
        """

        self.J = J
        self.h = h
        self.e = 0

        self.dimension = J.shape[0]

        self.ground_state = np.ones((self.dimension, 1))
        self.optimization_logs = dict()

        # Check parameters

        if assert_parameters:
            self.__assert__()

    def __assert__(self) -> None:

        """
        Checks the format of the attributes.

        Returns
        -------
        float
        """

        # Checking types
        assert isinstance(self.J,
                          np.ndarray), f"WARNING: The type of J must be a numpy array, instead got {type(self.J)}"
        assert isinstance(self.h,
                          np.ndarray), f"WARNING: The type of h must be a numpy array, instead got {type(self.h)}"

        # Checking dimensions
        assert self.J.shape[0] == self.J.shape[1], f"WARNING: J must be a square matrix, instead got {self.J.shape}"
        assert self.h.shape[0] == self.J.shape[
            0], f"WARNING: The dimension of h must fits J's, instead of {self.J.shape[0]} got {self.h.shape[0]}"
        assert self.h.shape[
                   1] == 1, f"WARNING: h must be a column vector with dimensions of this pattern: (n,1), instead got {self.h.shape}"

        # Checking J's properties
        assert np.allclose(self.J, self.J.T), "WARNING: J must be symmetric"
        # assert not np.any(self.J == np.zeros(self.J.shape)), "WARNING: J must not have null elements"

    def energy(self) -> float:

        """
        Computes the Ising energy of the model.

        Returns
        -------
        float
        """

        if self.ground_state is None:
            return None

        else:
            energy = -0.5 * self.ground_state.T @ self.J @ self.ground_state + self.ground_state.T @ self.h
            self.e = energy[0][0].copy()
            return energy[0][0]

    def get_ground_state(
            self,
            spin
    ) -> None:
        if np.sign(spin).any():
            self.ground_state = np.sign(spin)
